/*******************************************************************************
 * Copyright (c) 2003 Boris Pruessmann and others. All rights reserved. This
 * program and the accompanying materials are made available under the terms of
 * the Common Public License v0.5 which accompanies this distribution, and is
 * available at http://www.eclipse.org/legal/cpl-v05.html
 *
 * Contributors: Boris Pruessmann - Initial implementation.
 ******************************************************************************/
package net.sourceforge.perforce.ui.views;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import net.sourceforge.perforce.api.IError;
import net.sourceforge.perforce.api.IPerforceCommand;
import net.sourceforge.perforce.core.IClientUserListener;
import net.sourceforge.perforce.core.PerforceCorePlugin;
import net.sourceforge.perforce.ui.PerforceUIPlugin;
import net.sourceforge.perforce.ui.internal.ConsoleDocument;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.TextViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.LineStyleEvent;
import org.eclipse.swt.custom.LineStyleListener;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.part.ViewPart;

/**
 * @version $Revision: 489 $
 * @author <a href="mailto:bpruessmann@users.sourceforge.net">Boris
 *         Pruessmann</a>
 */
public class Console extends ViewPart {
  //----------------------------------------------------------------------------
  // Public Constants

  /** Unique identifier of the Console view. */
  public static final String ID = PerforceUIPlugin.ID + ".views.Console"; //$NON-NLS-1$

  // ---------------------------------------------------------------------------
  // Private Constants

  /** Time format for console output. */
  private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS");

  // --------------------------------------------------------------------
  // Private Static Variables

  /** Stores the document that holds the console text. */
  private static ConsoleDocument document;
  /** List of console instances. */
  private static List<Console> instances;
  /** Stores the client user listener. */
  private static IClientUserListener clientUserListener;

  // ---------------------------------------------------------------------------
  // Private Variables

  /** Stores the text viewer instance. */
  private TextViewer viewer;
  /** Stores the document listener. */
  private IDocumentListener documentListener;

  /** Color for commands. */
  private Color commandColor;
  /** Color for message. */
  private Color messageColor;
  /** Color for errors. */
  private Color errorColor;

  /** Action to clear the console's output. */
  private Action clearOutputAction;

  // -----------------------------------------------------------------------
  // Public Static Methods

  /** Called upon startup. */
  public static void startup() {
    document = new ConsoleDocument();
    instances = new ArrayList<Console>();

    clientUserListener = new ClientUserListener();
    PerforceCorePlugin.getPlugin().addClientUserListener(clientUserListener);
  }

  /** Called upon shutdown. */
  public static void shutdown() {
    PerforceCorePlugin.getPlugin().removeClientUserListener(clientUserListener);
    clientUserListener = null;
    document = null;
    instances = null;
  }

  //----------------------------------------------------------------------------
  // -- Public Methods

  @Override
  public void createPartControl(Composite parent) {
    Display display = Display.getCurrent();
    if (display == null) {
      display = Display.getDefault();
    }

    commandColor = createColor(display, new RGB(0x00, 0x00, 0x00));
    messageColor = createColor(display, new RGB(0x00, 0x00, 0x00));
    errorColor = createColor(display, new RGB(0xff, 0x00, 0x00));

    Composite composite = new Composite(parent, SWT.NULL);
    GridLayout layout = new GridLayout();
    layout.marginHeight = 0;
    layout.marginWidth = 0;
    composite.setLayout(layout);
    composite.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_FILL
        | GridData.HORIZONTAL_ALIGN_FILL));

    viewer = new TextViewer(composite, SWT.V_SCROLL | SWT.H_SCROLL);
    GridData data = new GridData(GridData.FILL_BOTH);
    viewer.getControl().setLayoutData(data);
    viewer.setEditable(false);
    viewer.setDocument(document);
    // TODO: viewer.getTextWidget().setFont(consoleFont);

    // add a line styler for colouring lines according to their type
    viewer.getTextWidget().addLineStyleListener(new LineStyleListener() {
      public void lineGetStyle(LineStyleEvent event) {
        StyleRange style =
            new StyleRange(event.lineOffset, event.lineText.length(),
                getConsoleLineColor(event.lineOffset), null);

        event.styles = new StyleRange[] {style};
      }
    });

    documentListener = new IDocumentListener() {
      public void documentAboutToBeChanged(DocumentEvent event) {
      }

      public void documentChanged(DocumentEvent event) {
        if (viewer == null) {
          return;
        }

        StyledText styledText = viewer.getTextWidget();
        if (!styledText.isDisposed()) {
          styledText.setTopIndex(styledText.getLineCount());
        }
      }
    };
    document.addDocumentListener(documentListener);

    contributeActions();

    instances.add(this);
  }

  @Override
  public void setFocus() {
    viewer.getTextWidget().setFocus();
  }

  @Override
  public void dispose() {
    if (documentListener != null) {
      document.removeDocumentListener(documentListener);
      documentListener = null;
    }

    instances.remove(this);
    super.dispose();

    if (commandColor != null) {
      commandColor.dispose();
      commandColor = null;
    }

    if (messageColor != null) {
      messageColor.dispose();
      messageColor = null;
    }

    if (errorColor != null) {
      errorColor.dispose();
      errorColor = null;
    }
  }

  //----------------------------------------------------------------------------
  // - Private Methods

  private void contributeActions() {
    final ImageDescriptor imageDescrptor =
        PerforceUIPlugin.getPlugin().getImageDescriptor(PerforceUIPlugin.IMG_CLEAR);
    clearOutputAction = new Action("Clear Console", imageDescrptor) {
      @Override
      public void run() {
        document.clear();
      }
    };

    IToolBarManager tbm = getViewSite().getActionBars().getToolBarManager();
    tbm.add(clearOutputAction);
    tbm.update(false);
  }

  /** Returns the color for the line containing the specified offset. */
  private Color getConsoleLineColor(int offset) {
    switch (document.getLineType(offset)) {
      case ConsoleDocument.COMMAND:
      case ConsoleDocument.DELIMITER:
        return commandColor;

      case ConsoleDocument.MESSAGE:
        return messageColor;

      case ConsoleDocument.ERROR:
        return errorColor;

      default:
        throw new IllegalStateException();
    }
  }

  /** Returns a color instance based on data from a preference field. */
  private Color createColor(Display display, RGB rgb) {
    return new Color(display, rgb);
  }

  /** Appends a line to the console if any views are open. */
  private static void appendConsoleLine(final int type, final String line) {
    Display display = Display.getCurrent();
    if (display == null) {
      display = Display.getDefault();
    }

    display.asyncExec(new Runnable() {
      public void run() {
        if (document == null) {
          return;
        }

        document.appendConsoleLine(type, line, instances.isEmpty());
      }
    });
  }

  // ----------------------------------------------------------------------
  // Private Nested Classes

  /** Implementation of IClientUserListener. */
  private static class ClientUserListener implements IClientUserListener {
    // -----------------------------------------------------------------------
    // Private Variables

    /**
     * Set containing the names of commands for which we want to display detail
     * information.
     */
    private Set<String> detailSet = new HashSet<String>();
    /** Set containing commands to exclude. */
    private Set<String> excludeSet = new HashSet<String>();
    /** Stores the starting times of commands. */
    private Map<IPerforceCommand, Long> startTimeMap = new Hashtable<IPerforceCommand, Long>();

    // ---------------------------------------------------------------------
    // Public Constructors

    /** Constructor for ClientUserListener. */
    public ClientUserListener() {
      detailSet.add("add"); //$NON-NLS-1$
      detailSet.add("delete"); //$NON-NLS-1$
      detailSet.add("edit"); //$NON-NLS-1$
      detailSet.add("revert"); //$NON-NLS-1$
      detailSet.add("submit"); //$NON-NLS-1$
      detailSet.add("sync"); //$NON-NLS-1$

      excludeSet.add("where"); //$NON-NLS-1$
    }

    //--------------------------------------------------------------------------
    // Public Methods

    /** @see IClientUserListener#run(IPerforceCommand, String[]) */
    public void run(IPerforceCommand command, String[] arguments) {
      if (isExcluded(command)) {
        return;
      }

      startTimeMap.put(command, new Long(System.currentTimeMillis()));

      StringBuilder buffer = new StringBuilder();
      buffer.append(TIME_FORMAT.format(new Date())).append("    "); //$NON-NLS-1$
      buffer.append("Executing p4 "); //$NON-NLS-1$
      buffer.append(command.getCommand()).append(" "); //$NON-NLS-1$
      for (int i = 0; i < arguments.length; i++) {
        buffer.append(arguments[i]).append(" "); //$NON-NLS-1$
      }

      appendConsoleLine(ConsoleDocument.COMMAND, buffer.toString());
    }

    /** @see IClientUserListener#handleError(IPerforceCommand, IError) */
    public void handleError(IPerforceCommand command, IError error) {
      if (isExcluded(command)) {
        return;
      }

      int type = ConsoleDocument.ERROR;
      switch (error.getSeverity()) {
        case IError.E_INFO:
        case IError.E_WARN:
          type = ConsoleDocument.MESSAGE;
          break;

        default:
          type = ConsoleDocument.ERROR;
          break;
      }

      final Date timestamp = new Date();
      StringTokenizer tokenizer = new StringTokenizer(error.format(IError.EF_PLAIN), "\n");
      while (tokenizer.hasMoreElements()) {
        final String message = (String) tokenizer.nextElement();

        StringBuilder buffer = new StringBuilder();
        buffer.append(TIME_FORMAT.format(timestamp)).append("    "); //$NON-NLS-1$

        buffer.append("    " + message); //$NON-NLS-1$


        appendConsoleLine(type, buffer.toString());
      }
    }

    public void outputError(IPerforceCommand command, String errorString) {
      if (isExcluded(command)) {
        return;
      }

      StringBuilder buffer = new StringBuilder();
      buffer.append(TIME_FORMAT.format(new Date())).append("    "); //$NON-NLS-1$
      buffer.append("    " + errorString); //$NON-NLS-1$

      appendConsoleLine(ConsoleDocument.ERROR, buffer.toString());
    }

    public void outputInfo(IPerforceCommand command, char level, String infoString) {
      if (isExcluded(command)) {
        return;
      }

      if (detailSet.contains(command.getCommand())) {
        StringBuilder buffer = new StringBuilder();
        buffer.append(TIME_FORMAT.format(new Date())).append("    "); //$NON-NLS-1$
        buffer.append("    " + infoString); //$NON-NLS-1$

        appendConsoleLine(ConsoleDocument.MESSAGE, buffer.toString());
      }
    }

    /** @see IClientUserListener#finished(IPerforceCommand) */
    public void finished(IPerforceCommand command) {
      if (isExcluded(command)) {
        return;
      }

      Long startTime = startTimeMap.remove(command);
      long duration = System.currentTimeMillis() - startTime.longValue();

      StringBuilder buffer = new StringBuilder();
      buffer.append(TIME_FORMAT.format(new Date())).append("    "); //$NON-NLS-1$
      buffer.append("    Took " + duration + " milliseconds."); //$NON-NLS-1$ //$NON-NLS-1$

      appendConsoleLine(ConsoleDocument.MESSAGE, buffer.toString());
      appendConsoleLine(ConsoleDocument.DELIMITER, ""); //$NON-NLS-1$
    }

    /** Checks whether the given command is excluded. */
    private boolean isExcluded(IPerforceCommand command) {
      return excludeSet.contains(command.getCommand());
    }
  }
}
